(function(root, factory) {
    if (typeof define === 'function' && define.amd) {
        define('smoothScroll', factory(root));
    } else if (typeof exports === 'object') {
        module.smoothScroll = factory(root);
    } else {
        root.smoothScroll = factory(root);
    }
})(this, function(root) {

    'use strict';

    //
    // Variables
    //

    var exports = {}; // Object for public APIs
    var supports = !!document.querySelector && !!root.addEventListener; // Feature test
    var settings;

    // Default settings
    var defaults = {
        speed: 500,
        easing: 'easeInOutCubic',
        offset: 0,
        updateURL: false,
        callbackBefore: function() {},
        callbackAfter: function() {}
    };


    //
    // Methods
    //

    /**
     * A simple forEach() implementation for Arrays, Objects and NodeLists
     * @private
     * @param {Array|Object|NodeList} collection Collection of items to iterate
     * @param {Function} callback Callback function for each iteration
     * @param {Array|Object|NodeList} scope Object/NodeList/Array that forEach is iterating over (aka `this`)
     */
    var forEach = function(collection, callback, scope) {
        if (Object.prototype.toString.call(collection) === '[object Object]') {
            for (var prop in collection) {
                if (Object.prototype.hasOwnProperty.call(collection, prop)) {
                    callback.call(scope, collection[prop], prop, collection);
                }
            }
        } else {
            for (var i = 0, len = collection.length; i < len; i++) {
                callback.call(scope, collection[i], i, collection);
            }
        }
    };

    /**
     * Merge defaults with user options
     * @private
     * @param {Object} defaults Default settings
     * @param {Object} options User options
     * @returns {Object} Merged values of defaults and options
     */
    var extend = function(defaults, options) {
        var extended = {};
        forEach(defaults, function(value, prop) {
            extended[prop] = defaults[prop];
        });
        forEach(options, function(value, prop) {
            extended[prop] = options[prop];
        });
        return extended;
    };

    /**
     * Calculate the easing pattern
     * @private
     * @param {String} type Easing pattern
     * @param {Number} time Time animation should take to complete
     * @returns {Number}
     */
    var easingPattern = function(type, time) {
        var pattern;
        if (type === 'easeInQuad') pattern = time * time; // accelerating from zero velocity
        if (type === 'easeOutQuad') pattern = time * (2 - time); // decelerating to zero velocity
        if (type === 'easeInOutQuad') pattern = time < 0.5 ? 2 * time * time : -1 + (4 - 2 * time) * time; // acceleration until halfway, then deceleration
        if (type === 'easeInCubic') pattern = time * time * time; // accelerating from zero velocity
        if (type === 'easeOutCubic') pattern = (--time) * time * time + 1; // decelerating to zero velocity
        if (type === 'easeInOutCubic') pattern = time < 0.5 ? 4 * time * time * time : (time - 1) * (2 * time - 2) * (2 * time - 2) + 1; // acceleration until halfway, then deceleration
        if (type === 'easeInQuart') pattern = time * time * time * time; // accelerating from zero velocity
        if (type === 'easeOutQuart') pattern = 1 - (--time) * time * time * time; // decelerating to zero velocity
        if (type === 'easeInOutQuart') pattern = time < 0.5 ? 8 * time * time * time * time : 1 - 8 * (--time) * time * time * time; // acceleration until halfway, then deceleration
        if (type === 'easeInQuint') pattern = time * time * time * time * time; // accelerating from zero velocity
        if (type === 'easeOutQuint') pattern = 1 + (--time) * time * time * time * time; // decelerating to zero velocity
        if (type === 'easeInOutQuint') pattern = time < 0.5 ? 16 * time * time * time * time * time : 1 + 16 * (--time) * time * time * time * time; // acceleration until halfway, then deceleration
        return pattern || time; // no easing, no acceleration
    };

    /**
     * Calculate how far to scroll
     * @private
     * @param {Element} anchor The anchor element to scroll to
     * @param {Number} headerHeight Height of a fixed header, if any
     * @param {Number} offset Number of pixels by which to offset scroll
     * @returns {Number}
     */
    var getEndLocation = function(anchor, headerHeight, offset) {
        var location = 0;
        if (anchor.offsetParent) {
            do {
                location += anchor.offsetTop;
                anchor = anchor.offsetParent;
            } while (anchor);
        }
        location = location - headerHeight - offset;
        return location >= 0 ? location : 0;
    };

    /**
     * Determine the document's height
     * @private
     * @returns {Number}
     */
    var getDocumentHeight = function() {
        return Math.max(
            document.body.scrollHeight, document.documentElement.scrollHeight,
            document.body.offsetHeight, document.documentElement.offsetHeight,
            document.body.clientHeight, document.documentElement.clientHeight
        );
    };

    /**
     * Remove whitespace from a string
     * @private
     * @param {String} string
     * @returns {String}
     */
    var trim = function(string) {
        return string.replace(/^\s+|\s+$/g, '');
    };

    /**
     * Convert data-options attribute into an object of key/value pairs
     * @private
     * @param {String} options Link-specific options as a data attribute string
     * @returns {Object}
     */
    var getDataOptions = function(options) {
        var settings = {};
        // Create a key/value pair for each setting
        if (options) {
            options = options.split(';');
            options.forEach(function(option) {
                option = trim(option);
                if (option !== '') {
                    option = option.split(':');
                    settings[option[0]] = trim(option[1]);
                }
            });
        }
        return settings;
    };

    /**
     * Update the URL
     * @private
     * @param {Element} anchor The element to scroll to
     * @param {Boolean} url Whether or not to update the URL history
     */
    var updateUrl = function(anchor, url) {
        if (history.pushState && (url || url === 'true')) {
            history.pushState({
                pos: anchor.id
            }, '', anchor);
        }
    };

    /**
     * Start/stop the scrolling animation
     * @public
     * @param {Element} toggle The element that toggled the scroll event
     * @param {Element} anchor The element to scroll to
     * @param {Object} settings
     * @param {Event} event
     */
    exports.animateScroll = function(toggle, anchor, options, event) {

        // Options and overrides
        var settings = extend(settings || defaults, options || {}); // Merge user options with defaults
        var overrides = getDataOptions(toggle ? toggle.getAttribute('data-options') : null);
        settings = extend(settings, overrides);

        // Selectors and variables
        var fixedHeader = document.querySelector('[data-scroll-header]'); // Get the fixed header
        var headerHeight = fixedHeader === null ? 0 : (fixedHeader.offsetHeight + fixedHeader.offsetTop); // Get the height of a fixed header if one exists
        var startLocation = root.pageYOffset; // Current location on the page
        var endLocation = getEndLocation(document.querySelector(anchor), headerHeight, parseInt(settings.offset, 10)); // Scroll to location
        var animationInterval; // interval timer
        var distance = endLocation - startLocation; // distance to travel
        var documentHeight = getDocumentHeight();
        var timeLapsed = 0;
        var percentage, position;

        // Prevent default click event
        if (toggle && toggle.tagName.toLowerCase() === 'a' && event) {
            event.preventDefault();
        }

        // Update URL
        updateUrl(anchor, settings.updateURL);

        /**
         * Stop the scroll animation when it reaches its target (or the bottom/top of page)
         * @private
         * @param {Number} position Current position on the page
         * @param {Number} endLocation Scroll to location
         * @param {Number} animationInterval How much to scroll on this loop
         */
        var stopAnimateScroll = function(position, endLocation, animationInterval) {
            var currentLocation = root.pageYOffset;
            if (position == endLocation || currentLocation == endLocation || ((root.innerHeight + currentLocation) >= documentHeight)) {
                clearInterval(animationInterval);
                settings.callbackAfter(toggle, anchor); // Run callbacks after animation complete
            }
        };

        /**
         * Loop scrolling animation
         * @private
         */
        var loopAnimateScroll = function() {
            timeLapsed += 16;
            percentage = (timeLapsed / parseInt(settings.speed, 10));
            percentage = (percentage > 1) ? 1 : percentage;
            position = startLocation + (distance * easingPattern(settings.easing, percentage));
            root.scrollTo(0, Math.floor(position));
            stopAnimateScroll(position, endLocation, animationInterval);
        };

        /**
         * Set interval timer
         * @private
         */
        var startAnimateScroll = function() {
            settings.callbackBefore(toggle, anchor); // Run callbacks before animating scroll
            animationInterval = setInterval(loopAnimateScroll, 16);
        };

        /**
         * Reset position to fix weird iOS bug
         * @link https://github.com/cferdinandi/smooth-scroll/issues/45
         */
        if (root.pageYOffset === 0) {
            root.scrollTo(0, 0);
        }

        // Start scrolling animation
        startAnimateScroll();

    };

    /**
     * Initialize Smooth Scroll
     * @public
     * @param {Object} options User settings
     */
    exports.init = function(options) {

        // feature test
        if (!supports) return;

        // Selectors and variables
        settings = extend(defaults, options || {}); // Merge user options with defaults
        var toggles = document.querySelectorAll('[data-scroll]'); // Get smooth scroll toggles

        // When a toggle is clicked, run the click handler
        forEach(toggles, function(toggle) {
            toggle.addEventListener('click', exports.animateScroll.bind(null, toggle, toggle.hash, settings), false);
        });

    };


    //
    // Public APIs
    //

    return exports;

});